/**
 * 
 */
package org.swing.utility.sytem.jar;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.jar.Pack200;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * @author lqnhu
 *
 */
public class Jar {
	private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; // java.util.jar.JarFile.MANIFEST_NAME
	private static final String ATTR_MANIFEST_VERSION = "Manifest-Version";
	private OutputStream os;
	private final Manifest manifest;
	private final JarInputStream jis;
	private JarOutputStream jos;
	private Pack200.Packer packer;
	private String jarPrefixStr;
	private Path jarPrefixFile;
	private boolean sealed;

	// <editor-fold defaultstate="collapsed" desc="Constructors">
	// ///////// Constructors ///////////////////////////////////
	/**
	 * Creates a new, empty, JAR
	 */
	public Jar() {
		this.jis = null;
		this.manifest = new Manifest();
	}

	/**
	 * Reads in the JAR from the given {@code InputStream}. Modifications will
	 * not be made to the original JAR file, but to a new copy, which is then
	 * written with {@link #write(OutputStream) write()}.
	 */
	public Jar(InputStream jar) throws IOException {
		this.jis = jar instanceof JarInputStream ? (JarInputStream) jar
				: newJarInputStream(jar);
		this.manifest = new Manifest(jis.getManifest());
	}

	/**
	 * Reads in the JAR from the given {@code Path}. Modifications will not be
	 * made to the original JAR file, but to a new copy, which is then written
	 * with {@link #write(OutputStream) write()}.
	 */
	public Jar(Path jar) throws IOException {
		this.jis = newJarInputStream(Files.newInputStream(jar));
		this.manifest = new Manifest(jis.getManifest());
	}

	/**
	 * Reads in the JAR from the given {@code File}. Modifications will not be
	 * made to the original JAR file, but to a new copy, which is then written
	 * with {@link #write(OutputStream) write()}.
	 */
	public Jar(File jar) throws IOException {
		this(jar.toPath());
	}

	/**
	 * Reads in the JAR from the given path. Modifications will not be made to
	 * the original JAR file, but to a new copy, which is then written with
	 * {@link #write(OutputStream) write()}.
	 */
	public Jar(String jar) throws IOException {
		this(Paths.get(jar));
	}

	/**
	 * Creates a copy
	 */
	public Jar(Jar jar) {
		this.jis = jar.jis;
		this.manifest = new Manifest(jar.manifest);
	}

	// </editor-fold>
	// <editor-fold defaultstate="collapsed" desc="Manifest">
	// ///////// Manifest ///////////////////////////////////
	/**
	 * Returns the manifest of this JAR. Modifications to the manifest will be
	 * reflected in the written JAR, provided they are done before any entries
	 * are added with {@code addEntry()}.
	 */
	public Manifest getManifest() {
		return manifest;
	}

	/**
	 * Sets an attribute in the main section of the manifest.
	 *
	 * @param name
	 *            the attribute's name
	 * @param value
	 *            the attribute's value
	 * @return {@code this}
	 * @throws IllegalStateException
	 *             if entries have been added or the JAR has been written prior
	 *             to calling this methods.
	 */
	public final Jar setAttribute(String name, String value) {
		verifyNotSealed();
		if (jos != null)
			throw new IllegalStateException(
					"Manifest cannot be modified after entries are added.");
		getManifest().getMainAttributes().putValue(name, value);
		return this;
	}

	/**
	 * Sets an attribute in a non-main section of the manifest.
	 *
	 * @param section
	 *            the section's name
	 * @param name
	 *            the attribute's name
	 * @param value
	 *            the attribute's value
	 * @return {@code this}
	 * @throws IllegalStateException
	 *             if entries have been added or the JAR has been written prior
	 *             to calling this methods.
	 */
	public final Jar setAttribute(String section, String name, String value) {
		verifyNotSealed();
		if (jos != null)
			throw new IllegalStateException(
					"Manifest cannot be modified after entries are added.");
		Attributes attr = getManifest().getAttributes(section);
		if (attr == null) {
			attr = new Attributes();
			getManifest().getEntries().put(section, attr);
		}
		attr.putValue(name, value);
		return this;
	}

	/**
	 * Sets an attribute in the main section of the manifest to a list. The list
	 * elements will be joined with a single whitespace character.
	 *
	 * @param name
	 *            the attribute's name
	 * @param values
	 *            the attribute's value
	 * @return {@code this}
	 * @throws IllegalStateException
	 *             if entries have been added or the JAR has been written prior
	 *             to calling this methods.
	 */
	public Jar setListAttribute(String name, Collection<?> values) {
		return setAttribute(name, join(values));
	}

	/**
	 * Sets an attribute in a non-main section of the manifest to a list. The
	 * list elements will be joined with a single whitespace character.
	 *
	 * @param section
	 *            the section's name
	 * @param name
	 *            the attribute's name
	 * @param values
	 *            the attribute's value
	 * @return {@code this}
	 * @throws IllegalStateException
	 *             if entries have been added or the JAR has been written prior
	 *             to calling this methods.
	 */
	public Jar setListAttribute(String section, String name,
			Collection<?> values) {
		return setAttribute(section, name, join(values));
	}

	/**
	 * Sets an attribute in the main section of the manifest to a map. The map
	 * entries will be joined with a single whitespace character, and each
	 * key-value pair will be joined with a '='.
	 *
	 * @param name
	 *            the attribute's name
	 * @param values
	 *            the attribute's value
	 * @return {@code this}
	 * @throws IllegalStateException
	 *             if entries have been added or the JAR has been written prior
	 *             to calling this methods.
	 */
	public Jar setMapAttribute(String name, Map<String, ?> values) {
		return setAttribute(name, join(values));
	}

	/**
	 * Sets an attribute in a non-main section of the manifest to a map. The map
	 * entries will be joined with a single whitespace character, and each
	 * key-value pair will be joined with a '='.
	 *
	 * @param section
	 *            the section's name
	 * @param name
	 *            the attribute's name
	 * @param values
	 *            the attribute's value
	 * @return {@code this}
	 * @throws IllegalStateException
	 *             if entries have been added or the JAR has been written prior
	 *             to calling this methods.
	 */
	public Jar setMapAttribute(String section, String name,
			Map<String, ?> values) {
		return setAttribute(section, name, join(values));
	}

	/**
	 * Returns an attribute's value from this JAR's manifest's main section.
	 *
	 * @param name
	 *            the attribute's name
	 */
	public String getAttribute(String name) {
		return getManifest().getMainAttributes().getValue(name);
	}

	/**
	 * Returns an attribute's value from a non-main section of this JAR's
	 * manifest.
	 *
	 * @param section
	 *            the manifest's section
	 * @param name
	 *            the attribute's name
	 */
	public String getAttribute(String section, String name) {
		Attributes attr = getManifest().getAttributes(section);
		return attr != null ? attr.getValue(name) : null;
	}

	/**
	 * Returns an attribute's list value from this JAR's manifest's main
	 * section. The attributes string value will be split on whitespace into the
	 * returned list. The returned list may be safely modified.
	 *
	 * @param name
	 *            the attribute's name
	 */
	public List<String> getListAttribute(String name) {
		return split(getAttribute(name));
	}

	/**
	 * Returns an attribute's list value from a non-main section of this JAR's
	 * manifest. The attributes string value will be split on whitespace into
	 * the returned list. The returned list may be safely modified.
	 *
	 * @param section
	 *            the manifest's section
	 * @param name
	 *            the attribute's name
	 */
	public List<String> getListAttribute(String section, String name) {
		return split(getAttribute(section, name));
	}

	/**
	 * Returns an attribute's map value from this JAR's manifest's main section.
	 * The attributes string value will be split on whitespace into map entries,
	 * and each entry will be split on '=' to get the key-value pair. The
	 * returned map may be safely modified.
	 *
	 * @param name
	 *            the attribute's name
	 */
	public Map<String, String> getMapAttribute(String name, String defaultValue) {
		return mapSplit(getAttribute(name), defaultValue);
	}

	/**
	 * Returns an attribute's map value from a non-main section of this JAR's
	 * manifest. The attributes string value will be split on whitespace into
	 * map entries, and each entry will be split on '=' to get the key-value
	 * pair. The returned map may be safely modified.
	 *
	 * @param section
	 *            the manifest's section
	 * @param name
	 *            the attribute's name
	 */
	public Map<String, String> getMapAttribute(String section, String name,
			String defaultValue) {
		return mapSplit(getAttribute(section, name), defaultValue);
	}

	// </editor-fold>
	// <editor-fold defaultstate="collapsed" desc="Entries">
	// ///////// Entries ///////////////////////////////////
	/**
	 * Adds an entry to this JAR.
	 *
	 * @param path
	 *            the entry's path within the JAR
	 * @param is
	 *            the entry's content
	 * @return {@code this}
	 */
	public Jar addEntry(String path, InputStream is) throws IOException {
		beginWriting();
		addEntry(jos, path, is);
		return this;
	}

	/**
	 * Adds an entry to this JAR.
	 *
	 * @param path
	 *            the entry's path within the JAR
	 * @param content
	 *            the entry's content
	 * @return {@code this}
	 */
	public Jar addEntry(String path, byte[] content) throws IOException {
		beginWriting();
		addEntry(jos, path, content);
		return this;
	}

	/**
	 * Adds an entry to this JAR.
	 *
	 * @param path
	 *            the entry's path within the JAR
	 * @param is
	 *            the entry's content
	 * @return {@code this}
	 */
	public Jar addEntry(Path path, InputStream is) throws IOException {
		return addEntry(path != null ? path.toString() : "", is);
	}

	/**
	 * Adds an entry to this JAR.
	 *
	 * @param path
	 *            the entry's path within the JAR
	 * @param file
	 *            the file to add as an entry
	 * @return {@code this}
	 */
	public Jar addEntry(Path path, Path file) throws IOException {
		return addEntry(path, Files.newInputStream(file));
	}

	/**
	 * Adds an entry to this JAR.
	 *
	 * @param path
	 *            the entry's path within the JAR
	 * @param file
	 *            the path of the file to add as an entry
	 * @return {@code this}
	 */
	public Jar addEntry(Path path, String file) throws IOException {
		return addEntry(path, Paths.get(file));
	}

	/**
	 * Adds an entry to this JAR.
	 *
	 * @param path
	 *            the entry's path within the JAR
	 * @param file
	 *            the file to add as an entry
	 * @return {@code this}
	 */
	public Jar addEntry(String path, File file) throws IOException {
		try (FileInputStream fos = new FileInputStream(file)) {
			return addEntry(path, fos);
		}
	}

	/**
	 * Adds an entry to this JAR.
	 *
	 * @param path
	 *            the entry's path within the JAR
	 * @param file
	 *            the path of the file to add as an entry
	 * @return {@code this}
	 */
	public Jar addEntry(String path, String file) throws IOException {
		try (FileInputStream fos = new FileInputStream(file)) {
			return addEntry(path, fos);
		}
	}

	/**
	 * Adds a class entry to this JAR.
	 *
	 * @param clazz
	 *            the class to add to the JAR.
	 * @return {@code this}
	 */
	public Jar addClass(Class<?> clazz) throws IOException {
		final String resource = clazz.getName().replace('.', '/') + ".class";
		return addEntry(resource,
				clazz.getClassLoader().getResourceAsStream(resource));
	}

	/**
	 * Adds a directory (with all its subdirectories) or the contents of a
	 * zip/JAR to this JAR.
	 *
	 * @param path
	 *            the path within the JAR where the root of the directory will
	 *            be placed, or {@code null} for the JAR's root
	 * @param dirOrZip
	 *            the directory to add as an entry or a zip/JAR file whose
	 *            contents will be extracted and added as entries
	 * @param filter
	 *            a filter to select particular classes
	 * @return {@code this}
	 */
	public Jar addEntries(Path path, Path dirOrZip, Filter filter)
			throws IOException {
		if (Files.isDirectory(dirOrZip))
			addDir(path, dirOrZip, filter, true);
		else {
			try (JarInputStream jis1 = newJarInputStream(Files
					.newInputStream(dirOrZip))) {
				addEntries(path, jis1, filter);
			}
		}
		return this;
	}

	/**
	 * Adds a directory (with all its subdirectories) or the contents of a
	 * zip/JAR to this JAR.
	 *
	 * @param path
	 *            the path within the JAR where the root of the directory will
	 *            be placed, or {@code null} for the JAR's root
	 * @param dirOrZip
	 *            the directory to add as an entry or a zip/JAR file whose
	 *            contents will be extracted and added as entries
	 * @return {@code this}
	 */
	public Jar addEntries(Path path, Path dirOrZip) throws IOException {
		return addEntries(path, dirOrZip, null);
	}

	/**
	 * Adds a directory (with all its subdirectories) or the contents of a
	 * zip/JAR to this JAR.
	 *
	 * @param path
	 *            the path within the JAR where the root of the directory/zip
	 *            will be placed, or {@code null} for the JAR's root
	 * @param dirOrZip
	 *            the directory to add as an entry or a zip/JAR file whose
	 *            contents will be extracted and added as entries
	 * @param filter
	 *            a filter to select particular classes
	 * @return {@code this}
	 */
	public Jar addEntries(String path, Path dirOrZip, Filter filter)
			throws IOException {
		return addEntries(path != null ? Paths.get(path) : null, dirOrZip,
				filter);
	}

	/**
	 * Adds a directory (with all its subdirectories) or the contents of a
	 * zip/JAR to this JAR.
	 *
	 * @param path
	 *            the path within the JAR where the root of the directory/zip
	 *            will be placed, or {@code null} for the JAR's root
	 * @param dirOrZip
	 *            the directory to add as an entry or a zip/JAR file whose
	 *            contents will be extracted and added as entries
	 * @return {@code this}
	 */
	public Jar addEntries(String path, Path dirOrZip) throws IOException {
		return addEntries(path, dirOrZip, null);
	}

	/**
	 * Adds the contents of the zip/JAR contained in the given byte array to
	 * this JAR.
	 *
	 * @param path
	 *            the path within the JAR where the root of the zip will be
	 *            placed, or {@code null} for the JAR's root
	 * @param zip
	 *            the contents of the zip/JAR file
	 * @return {@code this}
	 */
	public Jar addEntries(Path path, ZipInputStream zip) throws IOException {
		return addEntries(path, zip, null);
	}

	/**
	 * Adds the contents of the zip/JAR contained in the given byte array to
	 * this JAR.
	 *
	 * @param path
	 *            the path within the JAR where the root of the zip will be
	 *            placed, or {@code null} for the JAR's root
	 * @param zip
	 *            the contents of the zip/JAR file
	 * @param filter
	 *            a filter to select particular classes
	 * @return {@code this}
	 */
	public Jar addEntries(Path path, ZipInputStream zip, Filter filter)
			throws IOException {
		beginWriting();
		try (ZipInputStream zis = zip) {
			for (ZipEntry entry; (entry = zis.getNextEntry()) != null;) {
				final String target = path != null ? path.resolve(
						entry.getName()).toString() : entry.getName();
				if (target.equals(MANIFEST_NAME))
					continue;
				if (filter == null || filter.filter(target))
					addEntryNoClose(jos, target, zis);
			}
		}
		return this;
	}

	/**
	 * Adds the contents of a Java package to this JAR.
	 *
	 * @param clazz
	 *            A class whose package we wish to add to the JAR.
	 * @return {@code this}
	 */
	public Jar addPackageOf(Class<?> clazz) throws IOException {
		return addPackageOf(clazz, null);
	}

	/**
	 * Adds the contents of a Java package to this JAR.
	 *
	 * @param clazz
	 *            a class whose package we wish to add to the JAR.
	 * @param filter
	 *            a filter to select particular classes
	 * @return {@code this}
	 */
	public Jar addPackageOf(Class<?> clazz, Filter filter) throws IOException {
		try {
			final String path = clazz.getPackage().getName().replace('.', '/');
			URL dirURL = clazz.getClassLoader().getResource(path);
			if (dirURL != null && dirURL.getProtocol().equals("file"))
				addDir(Paths.get(path), Paths.get(dirURL.toURI()), filter,
						false);
			else {
				if (dirURL == null) // In case of a jar file, we can't actually
									// find a directory.
					dirURL = clazz.getClassLoader().getResource(
							clazz.getName().replace('.', '/') + ".class");
				if (dirURL.getProtocol().equals("jar")) {
					final URI jarUri = new URI(dirURL.getPath().substring(0,
							dirURL.getPath().indexOf('!')));
					try (JarInputStream jis1 = newJarInputStream(Files
							.newInputStream(Paths.get(jarUri)))) {
						for (JarEntry entry; (entry = jis1.getNextJarEntry()) != null;) {
							try {
								if (entry.getName().startsWith(path + '/')) {
									if (filter == null
											|| filter.filter(entry.getName()))
										addEntryNoClose(jos, entry.getName(),
												jis1);
								}
							} catch (ZipException e) {
								if (!e.getMessage().startsWith(
										"duplicate entry"))
									throw e;
							}
						}
					}
				} else
					throw new AssertionError();
			}
			return this;
		} catch (URISyntaxException e) {
			throw new AssertionError(e);
		}
	}

	private void addDir(final Path path, final Path dir1, final Filter filter,
			final boolean recursive) throws IOException {
		final Path dir = dir1.toAbsolutePath();
		Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
			@Override
			public FileVisitResult preVisitDirectory(Path d,
					BasicFileAttributes attrs) throws IOException {
				return (recursive || dir.equals(d)) ? FileVisitResult.CONTINUE
						: FileVisitResult.SKIP_SUBTREE;
			}

			@Override
			public FileVisitResult visitFile(Path file,
					BasicFileAttributes attrs) throws IOException {
				final Path p = dir.relativize(file.toAbsolutePath());
				final Path target = path != null ? path.resolve(p.toString())
						: p;
				if (!target.toString().equals(MANIFEST_NAME)) {
					if (filter == null || filter.filter(target.toString()))
						addEntry(target, file);
				}
				return FileVisitResult.CONTINUE;
			}
		});
	}

	private static void addEntry(JarOutputStream jarOut, String path,
			InputStream is) throws IOException {
		jarOut.putNextEntry(new JarEntry(path));
		copy(is, jarOut);
		jarOut.closeEntry();
	}

	private static void addEntryNoClose(JarOutputStream jarOut, String path,
			InputStream is) throws IOException {
		jarOut.putNextEntry(new JarEntry(path));
		copy0(is, jarOut);
		jarOut.closeEntry();
	}

	private static void addEntry(JarOutputStream jarOut, String path,
			byte[] data) throws IOException {
		jarOut.putNextEntry(new JarEntry(path));
		jarOut.write(data);
		jarOut.flush();
		jarOut.closeEntry();
	}

	// </editor-fold>
	// <editor-fold defaultstate="collapsed" desc="Jar File Properties">
	// ///////// Jar File Properties ///////////////////////////////////
	/**
	 * Sets a {@link Pack200} packer to use when writing the JAR.
	 *
	 * @param packer
	 * @return {@code this}
	 */
	public Jar setPacker(Pack200.Packer packer) {
		this.packer = packer;
		return this;
	}

	/**
	 * If set to true true, a header will be added to the JAR file when written,
	 * that will make the JAR an executable file in POSIX environments.
	 *
	 * @param value
	 * @return {@code this}
	 */
	public Jar setReallyExecutable(boolean value) {
		setJarPrefix(value ? "#!/bin/sh\n\nexec java -jar $0 \"$@\"\n" : null);
		return this;
	}

	/**
	 * Sets a string that will be prepended to the JAR file's data.
	 *
	 * @param value
	 *            the prefix, or {@code null} for none.
	 * @return {@code this}
	 */
	public Jar setJarPrefix(String value) {
		verifyNotSealed();
		if (jos != null)
			throw new IllegalStateException(
					"Really executable cannot be set after entries are added.");
		if (value != null && jarPrefixFile != null)
			throw new IllegalStateException("A prefix has already been set ("
					+ jarPrefixFile + ")");
		this.jarPrefixStr = value;
		return this;
	}

	/**
	 * Sets a file whose contents will be prepended to the JAR file's data.
	 *
	 * @param file
	 *            the prefix file, or {@code null} for none.
	 * @return {@code this}
	 */
	public Jar setJarPrefix(Path file) {
		verifyNotSealed();
		if (jos != null)
			throw new IllegalStateException(
					"Really executable cannot be set after entries are added.");
		if (file != null && jarPrefixStr != null)
			throw new IllegalStateException("A prefix has already been set ("
					+ jarPrefixStr + ")");
		this.jarPrefixFile = file;
		return this;
	}

	// </editor-fold>
	// <editor-fold defaultstate="collapsed" desc="Writing">
	// ///////// Writing ///////////////////////////////////
	/**
	 * Sets an {@link OutputStream} to which the JAR will be written. If used,
	 * this method must be called before any entries have been added or the JAR
	 * written. Calling this method prevents this object from using an internal
	 * buffer to store the JAR, and therefore, none of the other {@code write}
	 * methods can be called.
	 *
	 * @param os
	 *            the target OutputStream of this JAR.
	 * @return {@code this}
	 */
	public Jar setOutputStream(OutputStream os) {
		if (os == null)
			throw new NullPointerException("The OutputStream is null");
		if (jos != null)
			throw new IllegalStateException(
					"Entries have already been added, the JAR has been written or setOutputStream has already been called.");
		this.os = os;
		return this;
	}

	/**
	 * Same as {@link #setOutputStream(OutputStream)
	 * setOutputStream(Files.newOutputStream(out))}. If used, this method must
	 * be called before any entries have been added or the JAR written. Calling
	 * this method prevents this object from using an internal buffer to store
	 * the JAR, and therefore, none of the other {@code write} methods can be
	 * called.
	 *
	 * @param out
	 *            the target file to which this JAR will be written.
	 * @return {@code this}
	 */
	public Jar setOutput(Path out) throws IOException {
		return setOutputStream(Files.newOutputStream(out));
	}

	/**
	 * Same as {@link #setOutputStream(OutputStream) setOutputStream(new
	 * FileOutputStream(out))}. If used, this method must be called before any
	 * entries have been added or the JAR written. Calling this method prevents
	 * this object from using an internal buffer to store the JAR, and
	 * therefore, none of the other {@code write} methods can be called.
	 *
	 * @param out
	 *            the target file to which this JAR will be written.
	 * @return {@code this}
	 */
	public Jar setOutput(File out) throws IOException {
		return setOutputStream(new FileOutputStream(out));
	}

	private void beginWriting() throws IOException {
		verifyNotSealed();
		if (jos != null)
			return;
		if (os == null)
			this.os = new ByteArrayOutputStream();
		writePrefix(os);
		if (getAttribute(ATTR_MANIFEST_VERSION) == null)
			setAttribute(ATTR_MANIFEST_VERSION, "1.0");
		jos = new JarOutputStream(os, manifest);
		if (jis != null)
			addEntries(null, jis);
	}

	private void writePrefix(OutputStream os) throws IOException {
		if (jarPrefixStr != null) {
			final Writer out = new OutputStreamWriter(os, UTF_8);
			out.write(jarPrefixStr);
			out.flush();
		} else if (jarPrefixFile != null)
			Files.copy(jarPrefixFile, os);
		if (jarPrefixStr != null || jarPrefixFile != null) {
			os.write('\n');
			os.flush();
		}
	}

	public Jar close() throws IOException {
		if (sealed)
			return this;
		beginWriting();
		// writeManifest(); - some JDK Jar classes (like JarInputStream) assume
		// that the manifest must be the first entry
		jos.close();
		this.sealed = true;
		return this;
	}

	/**
	 * Writes this JAR to an output stream, and closes the stream.
	 */
	public <T extends OutputStream> T write(T os) throws IOException {
		close();
		if (!(this.os instanceof ByteArrayOutputStream))
			throw new IllegalStateException(
					"Cannot write to another target if setOutputStream has been called");
		final byte[] content = ((ByteArrayOutputStream) this.os).toByteArray();
		if (packer != null)
			packer.pack(new JarInputStream(new ByteArrayInputStream(content)),
					os);
		else
			os.write(content);
		os.close();
		return os;
	}

	private void verifyNotSealed() {
		if (sealed)
			throw new IllegalStateException(
					"This JAR has been sealed (when it was written)");
	}

	/**
	 * Writes this JAR to a file.
	 */
	public File write(File file) throws IOException {
		try (FileOutputStream fos = new FileOutputStream(file)) {
			write(fos);
			return file;
		}
	}

	/**
	 * Writes this JAR to a file.
	 */
	public Path write(Path path) throws IOException {
		write(Files.newOutputStream(path));
		return path;
	}

	/**
	 * Writes this JAR to a file.
	 */
	public void write(String file) throws IOException {
		write(Paths.get(file));
	}

	/**
	 * Returns this JAR file as an array of bytes.
	 */
	public byte[] toByteArray() {
		try {
			return write(new ByteArrayOutputStream()).toByteArray();
		} catch (IOException e) {
			throw new AssertionError();
		}
	}

	// </editor-fold>
	/**
	 * Turns a {@code String} into an {@code InputStream} containing the
	 * string's encoded characters.
	 *
	 * @param str
	 *            the string
	 * @param charset
	 *            the {@link Charset} to use when encoding the string.
	 * @return an {@link InputStream} containing the string's encoded
	 *         characters.
	 */
	public static InputStream toInputStream(String str, Charset charset) {
		return new ByteArrayInputStream(str.getBytes(charset));
	}

	// <editor-fold defaultstate="collapsed" desc="Filter">
	// ///////// Filter ///////////////////////////////////
	public static interface Filter {
		boolean filter(String entryName);
	}

	public static Filter matches(String regex) {
		final Pattern p = Pattern.compile(regex);
		return new Filter() {
			@Override
			public boolean filter(String entryName) {
				return p.matcher(entryName).matches();
			}
		};
	}

	public static Filter notMatches(String regex) {
		final Filter f = matches(regex);
		return new Filter() {
			@Override
			public boolean filter(String entryName) {
				return !f.filter(entryName);
			}
		};
	}

	// </editor-fold>
	// <editor-fold defaultstate="collapsed" desc="Utils">
	// ///////// Utils ///////////////////////////////////
	private static JarInputStream newJarInputStream(InputStream in)
			throws IOException {
		return new JarInputStream(in);
	}

	private static ZipInputStream newZipInputStream(InputStream in)
			throws IOException {
		return new ZipInputStream(in);
	}

	private static void copy(InputStream is, OutputStream os)
			throws IOException {
		try {
			copy0(is, os);
		} finally {
			is.close();
		}
	}

	private static void copy0(InputStream is, OutputStream os)
			throws IOException {
		final byte[] buffer = new byte[1024];
		for (int bytesRead; (bytesRead = is.read(buffer)) != -1;)
			os.write(buffer, 0, bytesRead);
		os.flush();
	}

	private static String join(Collection<?> list, String separator) {
		if (list == null)
			return null;
		StringBuilder sb = new StringBuilder();
		for (Object element : list)
			sb.append(element.toString()).append(separator);
		if (!list.isEmpty())
			sb.delete(sb.length() - separator.length(), sb.length());
		return sb.toString();
	}

	private static String join(Map<String, ?> map, char kvSeparator,
			String separator) {
		if (map == null)
			return null;
		StringBuilder sb = new StringBuilder();
		for (Map.Entry<String, ?> entry : map.entrySet())
			sb.append(entry.getKey()).append(kvSeparator)
					.append(entry.getValue().toString()).append(separator);
		if (!map.isEmpty())
			sb.delete(sb.length() - separator.length(), sb.length());
		return sb.toString();
	}

	private static String join(Collection<?> list) {
		return join(list, " ");
	}

	private static String join(Map<String, ?> map) {
		return join(map, '=', " ");
	}

	private static List<String> split(String str, String separator) {
		if (str == null)
			return null;
		String[] es = str.split(separator);
		final List<String> list = new ArrayList<>(es.length);
		for (String e : es) {
			e = e.trim();
			if (!e.isEmpty())
				list.add(e);
		}
		return list;
	}

	private static List<String> split(String list) {
		return split(list, " ");
	}

	private static Map<String, String> split(String map, char kvSeparator,
			String separator, String defaultValue) {
		if (map == null)
			return null;
		Map<String, String> m = new HashMap<>();
		for (String entry : split(map, separator)) {
			final String key = getBefore(entry, kvSeparator);
			String value = getAfter(entry, kvSeparator);
			if (value == null) {
				if (defaultValue != null)
					value = defaultValue;
				else
					throw new IllegalArgumentException("Element " + entry
							+ " in \"" + map
							+ "\" is not a key-value entry separated with "
							+ kvSeparator + " and no default value provided");
			}
			m.put(key, value);
		}
		return m;
	}

	private static Map<String, String> mapSplit(String map, String defaultValue) {
		return split(map, '=', " ", defaultValue);
	}

	private static String getBefore(String s, char separator) {
		final int i = s.indexOf(separator);
		if (i < 0)
			return s;
		return s.substring(0, i);
	}

	private static String getAfter(String s, char separator) {
		final int i = s.indexOf(separator);
		if (i < 0)
			return null;
		return s.substring(i + 1);
	}

	private static Path nullPath() {
		return null;
	}
	// </editor-fold>
}
